package com.atguigu.utils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 如何将这个类（切面类）中的这些方法（通知方法）动态的在目标方法（MyMathCalculator的目标方法）运行的各个位置切入
 *
 * 【1）、告知Spring此类是，切面类。使用@Aspect注解】
 * @author 伟哥
 * @date 2021-05-01 9:42
 */
@Aspect
@Component
@Order(1)
public class LogUtils {
    /**
     * 【2）、告诉Spring，每个方法都什么时候运行。】
     *
     * 5个通知注解
     * @Before 在目标方法之前运行                ：前置通知
     * @AfterReturing 在目标方法正常返回之后运行  ：返回通知
     * @AfterThrowing 在目标方法抛出异常之后运行  ：异常通知
     * @After 在目标运行结束之后运行              ：后置通知
     * @Around                                 ：环绕通知
     *
     * try{
     *      @Before
     *      method.invoke(obj,args);
     *      @AfterReturing
     * } catch(e){
     *      @AfterThrowing
     * } finally{
     *      @After
     * }
     *
     * 【3）、告诉Spring在哪一个方法切入】
     * 细节三：切入点表达式
     *
     * 切入点表达式的写法：
     * 固定格式：execution(访问权限付 返回值类型 方法全类名(参数表))
     *
     * 通配符：
     *      *:
     *          【方法名、类名】字符相关
     *          1）、匹配1个 或 多个字符
     *          execution(public int com.atguigu.impl.MyMath*r.*(int,int))
     *              MyMath*r类名，匹配，Mymath开头，r结尾。任意方法
     *
     *          2）、匹配任意1个参数
     *          execution(public int com.atguigu.impl.MyMath*r.*(int,*))
     *              方法的第一个参数是int，第二个参数 任意类型（匹配2个参数类型的方法）
     *
     *          3）、*在路径中，只能匹配1层路径
     *          execution(public int com.atguigu.*.MyMath*r.*(int,*))
     *              匹配com.atguigu包下的任意包
     *
     *          4）、*不能表示权限，把权限去掉不写，就是任意权限（实际Spring只能切入public，所以就是public是可选的，写不写随便）
     *
     *      ..：
     *          【参数、路径】
     *          1）、匹配任意多个参数，任意参数类型
     *          execution(public int com.atguigu.impl.MyMath*r.add(..))
     *
     *          2）、匹配 任意 多层路径
     *          execution(public int com.atguigu..MyMath*r.add(..))
     *
     * 记住两种：
     *      最精确：execution(public int com.atguigu.impl.MyMatchCalculator.add(int,int))
     *
     *      最模糊：execution(* *(..))【千万别写，知道就行了】
     *                       第一个*，任意返回类型（权限修饰符public，直接不写）
     *                       第二个*，任意包，任意类，任意方法
     *                       (..)，任意方法参数列表
     *             execution(* *.*(..))【这个写法看起来容易理解一些】
     *
     * &&、||、！【与或非】
     *
     * &&：我们要切入的位置，同时满足这两个表达式
     * execution(public int com.atguigu..MyMatch*.*(..))&&execution(* *.*(int,int))
     *
     * ||：我们要切入的位置，满足任意1个表达式
     * execution(public int com.atguigu..MyMatch*.*(..))||execution(* *.*(int,int))
     *
     * !：只要不是这个表达式的，都切入
     * !execution(* *.*(int,int))
     *
     *
     * 细节四：
     *      通知方法的执行顺序
     *
     *      正常执行：@Before（前置通知）--》 【目标方法】 ---》 @AfterReturning（返回通知） ---》 @After（后置最终通知）
     *
     *      异常执行：@Before（前置通知）--》 【目标方法】 ---》 @AfterReturning（异常通知） ---》 @After（后置最终通知）
     *
     *
     * 细节五：【JoinPoint】在通知方法运行的时候，拿到目标方法的详细信息
     *     1）只需要为通知方法的参数列表上，写一个参数：JoinPoint joinPoint
     *          JoinPoint joinPoint：封装了当前目标方法的详细信息
     *     // 获取，参数列表
     *     joinPoint.getArgs();
     *     // 获取，方法签名
     *     Signature signature = joinPoint.getSignature();
     *          //获取 方法名
     *     String methodName = signature.getName();
     *          //获取 方法修饰符
     *     int modifiers = signature.getModifiers();
     *          //获取 方法返回值类型的名字
     *     String declaringTypeName = signature.getDeclaringTypeName();
     *          //获取 方法的返回值类型
     *     Class declaringType = signature.getDeclaringType();
     *
     *
     * 细节六：获取返回值【返回通知才能获取返回值，因为运行返回通知的时候，目标方法已经产生了返回值】
     *      1、在切入点方法的参数列表添加一个参数，名称随意，例如：Object result
     *          public static void logEnd(JoinPoint joinPoint,Object result) {
     *      2、在通知注解，添加属性returning="刚才添加的参数名"
     *          @AfterReturning(value = "execution(public * com.atguigu..*.*(..))",returning = "result")
     *          public static void logEnd(Object result) {
     *      3、Spring会自动使用result参数接收，返回值
     *      4、Object result：指定Spring接收的返回值类型。
     *          比如：Double 只能接收浮点型，如果不是浮点型，返回值就会被丢弃
     *          所以接收的类型，写大一些，例如：Object
     *
     * 细节七：获取异常，同获取返回值【异常通知，才能获取】
     *      1、在切入点方法的参数列表添加一个参数，名称随意，例如：Exception exception
     *          public static void logException(Exception exception) {
     *      2、在通知注解，添加属性returning="刚才添加的参数"
     *          @AfterThrowing(value = "execution(public * com.atguigu..*.*(..))",throwing = "exception")
     *          public static void logException(Exception exception) {
     *      3、Spring会自动使用exception参数接收，异常信息
     *      4、Exception exception：指定通知方法可以接收哪些异常，
     *              比如：NullPointException只能接收空指针异常，所以这个异常类型写大一些，比如：Exception
     *
     * 细节八：Spring对通知方法的书写要求不严格
     *      权限是private、返回值int，返回0； 会照常运行
     *      唯一要求：方法的参数列表，一定不能乱写
     *          通知方法是Spring利用反射调用的，每次方法调用都要确定这个方法参数列表的值。
     *          参数表上的每一个参数，Spring都要知道是什么。
     *          JoinPoint:认识
     *          不认识的参数，报错
     *              所以，Object result、Exception exception，用来获取返回值、异常信息；
     *              都需要在通知注解上，告知Spring，这是什么
     *
     * 细节九：抽取可重用的切入点表达式【@Pointcut注解】
     *  步骤：1）、随便声明一个没有实现，返回void的空方法
     *       2）、在，此方法上，使用注解@Pointcut
     *       @Pointcut("execution(public * com.atguigu..*.*(..))")
     *       public void hahaMyPoint(){}
     *       3)、在使用切入点表达式的地方，填入，声明的方法名
     *       @Before("hahaMyPoint()")
     *       public static void logStart(JoinPoint joinPoint){
     *
     * 细节十：环绕通知【动态代理】
     * @Around：环绕通知 ：是Spring中最强大的通知
     *      本质就是，手写版的【动态代理】
     *
     *      try{
     *          前置通知
     *          method.invoke(obj,args);
     *          返回通知
     *      } catch(Exception e){
     *          异常通知
     *      } finally{
     *          后置通知
     *      }
     *
     *      四合一通知，就是环绕通知
     *      环绕通知中，有一个参数ProceedingJoinPoint pjp 它是JoinPoint的子接口
     *          @Around("hahaMyPoint()")
     *          public void myAround(ProceedingJoinPoint pjp){
     *
     *
     * 【环绕通知】 和 【普通通知】的【顺序】
     * 正常执行：
     *      环绕 前置
     *      普通 前置
     *      【目标方法执行】
     *      普通 返回
     *      普通 最终
     *      环绕 返回
     *      环绕 最终
     *
     * 错误执行：
     *      环绕 前置
     *      普通 前置
     *      【目标方法执行】
     *      普通 异常
     *      普通 最终
     *      环绕 异常
     *      环绕 最终
     *
     * 细节十一：多切面的，切入顺序【@Order注解】
     *
     * 正常执行：
     *      ValidateAspect前置通知
     *      环绕前置通知：方法名：div
     *      （前置通知）【div】方法开始执行，用的参数列表:[2, 1]
     *      除法
     *      （返回通知）方法正常执行完成，结果是：2
     *      （最终通知）【div】方法最终结束了！
     *      环绕返回通知：结果是：2
     *      环绕最终通知：方法结束！
     *      ValidateAspect返回通知
     *      ValidateAspect最终通知
     * 异常执行：
     *      ValidateAspect前置通知
     *      环绕前置通知：方法名：div
     *      （前置通知）【div】方法开始执行，用的参数列表:[2, 0]
     *      除法
     *      （异常通知）方法出现的代理异常：java.lang.ArithmeticException: / by zero
     *      （最终通知）【div】方法最终结束了！
     *      环绕异常通知：异常是：java.lang.ArithmeticException: / by zero
     *      环绕最终通知：方法结束！
     *      ValidateAspect异常通知
     *      ValidateAspect最终通知
     *
     * 如何排序？
     *      方式一：在切面类上，使用@Order注解，默认是int的最大值
     *      方式二：默认是jar包内的 按类名，字母排序，谁在前面，谁是第一个
     */
    /*
    想在执行目标方法之前运行
    3）、那么，在哪一个方法之前运行呢？所以接下来，就要写，切入点表达式
    execution(访问权限付 返回值类型 方法签名)

    @Before("execution(public int com.atguigu.impl.MyMathCalculator.add(int,int))")
     */
    @Before("hahaMyPoint()")
    public static void logStart(JoinPoint joinPoint){
        System.out.println("（前置通知）【"+joinPoint.getSignature().getName()+"】方法开始执行，用的参数列表:"
                +Arrays.asList(joinPoint.getArgs()));
    }

    /**
     *
     * @param result 接收目标方法的返回值
     */
    //想在目标方法正常执行完成之后运行
    @AfterReturning(value = "hahaMyPoint()",returning = "result")
    public static void logEnd(Object result) {
        System.out.println("（返回通知）方法正常执行完成，结果是："+result);
    }

    /**
     *
     * @param exception 接收异常信息
     */
    //方法出现异常的时候运行
    @AfterThrowing(value = "hahaMyPoint()",throwing = "exception")
    public static void logException(Exception exception) {
        /*
        这里的异常信息，直接打印即可，不会出现代理对象异常，而是直接的异常信息by zero，所以不需要exception.getCause()
        System.out.println("（异常通知）方法出现的代理异常："+exception);
        System.out.println("异常原因是："+exception.getCause());
         */
        System.out.println("（异常通知）方法出现的代理异常："+exception);
    }

    //最后执行
    @After("hahaMyPoint()")
    public static void logFinal(JoinPoint joinPoint) {
        System.out.println("（最终通知）【"+joinPoint.getSignature().getName()+"】方法最终结束了！");
    }

    /**
     * 细节九：抽取可重用的切入点表达式
     */
    @Pointcut("execution(public * com.atguigu..*.*(..))")
    public void hahaMyPoint(){}

    /**
     * 细节十：环绕通知
     *

     *
     */
    @Around("hahaMyPoint()")
    public Object myAround(ProceedingJoinPoint pjp){

        // 获取方法参数
        Object[] args = pjp.getArgs();

        // 获取方法名
        String methodName = pjp.getSignature().getName();

        /* 就是利用反射调用目标方法即可
        pjp.proceed(args);等价于 method.invoke(目标对象，参数列表)
         */
        Object result = null;
        try {
            //@Before
            System.out.println("环绕前置通知：方法名："+pjp.getSignature().getName());
            //等价于 method.invoke(目标对象，参数列表)
            result = pjp.proceed(args);
            //@AfterReturning
            System.out.println("环绕返回通知：结果是："+result);
        } catch (Throwable throwable) {
            //@AfterThrowing
            System.out.println("环绕异常通知：异常是："+throwable);
            //提示：为了让外界能感知这个异常，将此异常跑出去
            throw new RuntimeException(throwable);

        } finally {
            //@After
            System.out.println("环绕最终通知：方法结束！");
        }

        //反射调用之后，产生的返回值
        return result;
    }
}
